package de.mmis.core.pojop.processor;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import de.mmis.core.base.GetJarData;
import de.mmis.core.base.MethodDescriptor;
import de.mmis.core.base.ObjectDescriptor;
import de.mmis.core.base.Toolbox;
import de.mmis.core.base.abstracttree.InnerNode;
import de.mmis.core.base.abstracttree.LeafNode;
import de.mmis.core.base.abstracttree.LeafNode.Encoding;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.MethodInvocationException;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.NoMatchingMethodFoundException;
import de.mmis.core.base.event.AbstractObservable;
import de.mmis.core.base.event.Event;
import de.mmis.core.base.event.Observable;
import de.mmis.core.base.event.Observer;
import de.mmis.core.pojop.answer.AbstractAnswer;
import de.mmis.core.pojop.answer.AbstractAnswer.Type;
import de.mmis.core.pojop.request.AbstractRequest;
import de.mmis.core.pojop.request.Request;

public abstract class AbstractRequestProcessorFactory<R extends Request>
		implements RequestProcessorFactory<R> {

	protected ObjectDescriptor objectDescriptor;
	protected String objectID;
	protected Map<String, Object> objectProperties;
	protected ClassLoader classLoader;

	protected Observable<Event> observableObject;

	protected final boolean explicit;

	@SuppressWarnings("unchecked")
	public AbstractRequestProcessorFactory(ObjectDescriptor objectDescriptor,
			String objectID, Map<String, Object> objectProperties,
			boolean explicit) {
		this.objectDescriptor = objectDescriptor;
		this.objectID = objectID;
		this.objectProperties = objectProperties;
		this.classLoader = objectDescriptor.getObjectClass().getClassLoader();
		this.explicit = explicit;

		// enable observation of observable objects
		if (this.objectDescriptor.getObservableClass() != null)
			this.observableObject = (Observable<Event>) this.objectDescriptor
					.getObject();
		else
			this.observableObject = null;
	}

	public abstract class AbstractRequestProcessor extends
			AbstractObservable<RequestProcessorFactory.RequestProcessorEvent>
			implements RequestProcessor<R>, Observer<Event> {
		protected List<Tree> filterlist;

		public AbstractRequestProcessor() {
			this.filterlist = Collections
					.synchronizedList(new ArrayList<Tree>());
		}

		@Override
		public void init() {
			// start observation of observable objects
			if (AbstractRequestProcessorFactory.this.observableObject != null)
				AbstractRequestProcessorFactory.this.observableObject
						.addObserver(this);
		}

		public AbstractAnswer answerAbstractRequest(AbstractRequest request) {
			switch (request.getType()) {
			case MethodCall:
				return callMethod(request.getData());
			case Observe:
				return startObservation(request.getData());
			case Deobserve:
				return stopObservation(request.getData());
			case Help:
				if (request.getData() == null)
					return generateHelpMessage();

				if (request.getData().getType() != Tree.Type.LEAF)
					return new AbstractAnswer("method name must be an Atom");
				return generateMethodHelp(((LeafNode) request.getData())
						.getUTF8Data());
			case Quit:
				return new AbstractAnswer(Type.Quit);
			case Type:
				return new AbstractAnswer(
						Tree.fromObject(
								new de.mmis.core.base.Type(
										AbstractRequestProcessorFactory.this.objectDescriptor
												.getObjectClass()), explicit));
			case Interfaces:
				Class<?>[] interfaceClasses = AbstractRequestProcessorFactory.this.objectDescriptor
						.getObjectInterfaces();
				de.mmis.core.base.Type[] ret = new de.mmis.core.base.Type[interfaceClasses.length];
				for (int i = 0; i < ret.length; i++)
					ret[i] = new de.mmis.core.base.Type(interfaceClasses[i]);

				return new AbstractAnswer(Tree.fromObject(ret, explicit));
			case Properties:
				return new AbstractAnswer(Tree.fromObject(
						AbstractRequestProcessorFactory.this.objectProperties,
						explicit));
			case JarData:
				return new AbstractAnswer(
						Tree.fromObject(
								GetJarData
										.getJarData(AbstractRequestProcessorFactory.this.objectDescriptor
												.getObjectClass()), explicit));
			case Echo:
				return new AbstractAnswer(request.getData());
			case Error:
				return new AbstractAnswer(request.getException());
			case ListMethods:
				return generateMethodList();
			}
			return null;
		}

		public AbstractAnswer generateMethodList() {
			List<Tree> ret = new ArrayList<Tree>();
			for (MethodDescriptor md : AbstractRequestProcessorFactory.this.objectDescriptor
					.getMethodDescriptors()) {
				InnerNode t = new InnerNode();
				t.add(Tree.fromObject(md.getName()));
				t.add(Tree.fromObject(md.getReturnType()));
				for (int i = 0; i < md.getArgumentCount(); i++)
					t.add(new InnerNode(Tree.fromObject(md.getArgumentName(i)),
							Tree.fromObject(md.getParameterType(i))));
				ret.add(t);
			}
			return new AbstractAnswer(Tree.fromObject(ret));
		}

		public AbstractAnswer callMethod(Tree methodCall) {
			Tree answer;
			try {
				answer = SerializerHelper.invokeObjectMethod(
						AbstractRequestProcessorFactory.this.objectDescriptor,
						methodCall,
						AbstractRequestProcessorFactory.this.classLoader,
						new HashMap<String, Object>()).toTree(explicit);
			} catch (MethodInvocationException e) {
				if (e.getCause() == null)
					return new AbstractAnswer(e);
				if (!(e.getCause() instanceof InvocationTargetException)
						|| e.getCause().getCause() == null)
					return new AbstractAnswer(e.getCause());
				return new AbstractAnswer(e.getCause().getCause());

			} catch (NoMatchingMethodFoundException e) {
				return new AbstractAnswer(e);
			}

			return new AbstractAnswer(answer != null ? answer : new LeafNode(
					":void"));
		}

		@Override
		public void close() {
			if (AbstractRequestProcessorFactory.this.observableObject != null)
				AbstractRequestProcessorFactory.this.observableObject
						.removeObserver(this);
		}

		// //
		// observation and event handling methods
		// //
		/**
		 * starting observation of object events
		 * 
		 * @param exp
		 *            S-Expression defining event filter
		 * @throws IOException
		 *             an error occurred while writing to output stream
		 */
		protected AbstractAnswer startObservation(Tree exp) {
			if (AbstractRequestProcessorFactory.this.observableObject == null)
				return new AbstractAnswer("This object is not observable");

			synchronized (this.filterlist) {
				if (exp == null)
					this.filterlist.clear();
				this.filterlist.add(exp == null ? new LeafNode(
						new byte[] { '?' }, Encoding.Token) : exp);
			}
			return new AbstractAnswer(Type.OK);
		}

		/**
		 * stopping observation of object events
		 * 
		 * @param exp
		 *            S-Expression defining event filter
		 * @throws IOException
		 *             an error occurred while writing to output stream
		 */
		protected AbstractAnswer stopObservation(Tree exp) {
			if (AbstractRequestProcessorFactory.this.observableObject == null)
				return new AbstractAnswer("This object is not observable");

			synchronized (this.filterlist) {
				if (exp == null) {
					this.filterlist.clear();
					return new AbstractAnswer(Type.OK);
				}
				for (Tree e : this.filterlist) {
					if (exp.equals(e)) {
						this.filterlist.remove(e);
						return new AbstractAnswer(Type.OK);
					}
				}
			}
			return new AbstractAnswer("specified filter not found");
		}

		@Override
		public void notify(Observable<? extends Event> sender, Event event) {
			notify(Tree.fromObject(event, explicit));
		}

		protected void notify(Tree exp) {
			boolean notify = false;
			for (Tree pattern : this.filterlist)
				notify |= exp.matchPattern(pattern);

			if (notify)
				processEvent(exp);
		}

		protected abstract void processEvent(Tree exp);

		// //
		// help output methods
		// //
		public AbstractAnswer generateHelpMessage() {
			StringBuilder sb = new StringBuilder();
			sb.append("The following object is published on this socket:\n\n"
					+ "  ID     : "
					+ AbstractRequestProcessorFactory.this.objectID
					+ "\n"
					+ "  Class  : "
					+ AbstractRequestProcessorFactory.this.objectDescriptor
							.getObjectClass().getName()
					+ "\n"
					+ "  Object : "
					+ AbstractRequestProcessorFactory.this.objectDescriptor
							.getObject() + "\n");
			if (AbstractRequestProcessorFactory.this.objectDescriptor
					.getObservableClass() != null)
				sb.append("  The object is observable giving events of type "
						+ AbstractRequestProcessorFactory.this.objectDescriptor
								.getObservableClass().getSimpleName() + "\n");

			if (AbstractRequestProcessorFactory.this.objectDescriptor
					.getClassHelp() != null)
				sb.append("\nhelp text: "
						+ AbstractRequestProcessorFactory.this.objectDescriptor
								.getClassHelp() + "\n");

			sb.append("\nOffering the following methods:\n\n");

			for (MethodDescriptor methodDescriptor : AbstractRequestProcessorFactory.this.objectDescriptor
					.getMethodDescriptors()) {
				sb.append("  " + methodDescriptor + "\n");
			}

			return new AbstractAnswer(Toolbox.toUTF8Bytes(sb.toString()));
		}

		public AbstractAnswer generateMethodHelp(String methodName) {
			boolean found = false;
			StringBuilder sb = new StringBuilder();
			for (MethodDescriptor methodDescriptor : AbstractRequestProcessorFactory.this.objectDescriptor
					.getMethodDescriptors()) {
				if (methodDescriptor.getName().equals(methodName)) {
					sb.append(methodDescriptor.toString(true) + "\n\n");
					found = true;
				}
			}
			if (!found)
				return new AbstractAnswer("no method with given name found");
			return new AbstractAnswer(Toolbox.toUTF8Bytes(sb.toString()));
		}
	}
}
